{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Recurrent Networks"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this notebook we'll see how to use recurrent networks to create a character-level language model for text generation.  We'll start with a simple fully-connected network and show how it can be used as an \"unrolled\" recurrent layer, then gradually build up from there until we have a model capable of generating semi-reasonable sounding text.  Much of this content is based on Jeremy Howard's [fast.ai lessons](http://course.fast.ai/), specifically lesson 6 from the first course.  However, we'll use Keras instead of PyTorch and build out all of the code from scratch rather than relying on the fast.ai library.\n",
    "\n",
    "The text corpus we're using for this task are the works of the philosopher Nietzsche.  The whole corpus can be found [here](https://s3.amazonaws.com/text-datasets/nietzsche.txt).  Let's start by loading the data into memory and taking a peek at the beginning of the text."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "600893"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "%matplotlib inline\n",
    "import io\n",
    "import numpy as np\n",
    "import keras\n",
    "from keras.utils.data_utils import get_file\n",
    "\n",
    "path = get_file('nietzsche.txt', origin='https://s3.amazonaws.com/text-datasets/nietzsche.txt')\n",
    "with io.open(path, encoding='utf-8') as f:\n",
    "    text = f.read().lower()\n",
    "\n",
    "len(text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'preface\\n\\n\\nsupposing that truth is a woman--what then? is there not ground\\nfor suspecting that all philosophers, in so far as they have been\\ndogmatists, have failed to understand women--that the terrible\\nseriousness and clumsy importunity with which they have usually paid\\ntheir addresses to truth, have been unskilled and unseemly methods for\\nwinning a woman? certainly she has never allowed herself '"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "text[:400]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Get the unique set of characters that appear in the text.  This is our vocabulary."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "57"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "chars = sorted(list(set(text)))\n",
    "vocab_size = len(chars)\n",
    "vocab_size"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'\\n !\"\\'(),-.0123456789:;=?[]_abcdefghijklmnopqrstuvwxyzäæéë'"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "''.join(chars)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Create a dictionary that maps each unique character to an integer, which is what we'll feed into the model.  The actual integer used isn't important, it just has to be unique (here we just take the index from the \"chars\" list above).  It's also useful to have a reverse mapping to get back to characters in order to do something with the model output.  Finally, create a \"mapped\" corpus where each character in the data has been replaced with its corresponding integer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[42, 44, 31, 32, 27, 29, 31, 0, 0, 0, 45, 47, 42, 42, 41, 45, 35, 40, 33, 1]"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "char_indices = {c: i for i, c in enumerate(chars)}\n",
    "indices_char = {i: c for i, c in enumerate(chars)}\n",
    "idx = [char_indices[c] for c in text]\n",
    "\n",
    "idx[:20]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Example of how to convert from integers back to characters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'preface\\n\\n\\nsupposing that truth is a woman--what then? is there not ground\\nfor suspecting that all ph'"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "''.join(indices_char[i] for i in idx[:100])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For our first attempt, we'll build a model that accepts a 3-character sequence as input and tries to predict the following character in the text.  For simplicity, we can just manually create each character sequence. Start by creating lists that take every 3rd character, offset by some amount between 0 and 3."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "cs = 3\n",
    "c1 = [idx[i] for i in range(0, len(idx) - cs, cs)]\n",
    "c2 = [idx[i + 1] for i in range(0, len(idx) - cs, cs)]\n",
    "c3 = [idx[i + 2] for i in range(0, len(idx) - cs, cs)]\n",
    "c4 = [idx[i + 3] for i in range(0, len(idx) - cs, cs)]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This just converts the lists to numpy arrays.  Notice that this approach resulted in non-overlapping sequences, i.e. we use characters 0-2 to predict character 3, then characters 3-5 to predict character 6, etc.  That's why the array shape is about 1/3 the size of the original text.  We'll see how to improve on this later."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((200297,), (200297,))"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "x1 = np.stack(c1)\n",
    "x2 = np.stack(c2)\n",
    "x3 = np.stack(c3)\n",
    "y = np.stack(c4)\n",
    "\n",
    "x1.shape, y.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Our model will use embeddings to represent each character.  This is why we converted them to integers before - each integer gets turned into a vector in the embedding layer.  Set some variables for the embedding vector size and the number of hidden units to use in the model.  Finally, we need to convert the target variable to a one-hot character encoding.  This is because the model outputs a probability for each character, and in order to score this properly it needs to be able to compare that output with an array that's structured the same way."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(200297, 56)"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "n_factors = 42\n",
    "n_hidden = 256\n",
    "y_cat = keras.utils.to_categorical(y)\n",
    "\n",
    "y_cat.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we get to the first iteration of our model.  The way I've structred this is by defining the layers of the model so that they can be re-used across multiple inputs.  For example, rather than create an embedding layer for each of the three character inputs, we're instead creating one embedding layer and sharing it.  This is a reasonable approach to handling sequences since each input comes from an identical distribution.\n",
    "\n",
    "The next thing to observe is the part where h is defined.  The first character is fed through the hidden layer like normal, but the other characters in the sequence are doing something different.  We're re-using the same layer, but instead of just taking the character as input, we're using the character + the previous output h.  This is the \"hidden\" state of the model.  I think about it in the following way: \"give me the output of this layer for character c conditioned on the fact that these other characters (represented by h) came before it\".\n",
    "\n",
    "You'll notice that there's no use of an RNN class at all.  Basically what's going on here is we're implmenting an \"unrolled\" RNN from scratch on our own."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras import backend as K\n",
    "from keras.models import Model\n",
    "from keras.layers import add\n",
    "from keras.layers import Input, Reshape, Dense, Add\n",
    "from keras.layers.embeddings import Embedding\n",
    "from keras.optimizers import Adam\n",
    "\n",
    "def Char3Model(vocab_size, n_factors, n_hidden):\n",
    "    embed_layer = Embedding(vocab_size, n_factors)\n",
    "    reshape_layer = Reshape((n_factors,))\n",
    "    input_layer = Dense(n_hidden, activation='relu')\n",
    "    hidden_layer = Dense(n_hidden, activation='tanh')\n",
    "    output_layer = Dense(vocab_size - 1, activation='softmax')\n",
    "\n",
    "    in1 = Input(shape=(1,))\n",
    "    in2 = Input(shape=(1,))\n",
    "    in3 = Input(shape=(1,))\n",
    "\n",
    "    c1 = input_layer(reshape_layer(embed_layer(in1)))\n",
    "    c2 = input_layer(reshape_layer(embed_layer(in2)))\n",
    "    c3 = input_layer(reshape_layer(embed_layer(in3)))\n",
    "\n",
    "    h = hidden_layer(c1)\n",
    "    h = hidden_layer(add([h, c2]))\n",
    "    h = hidden_layer(add([h, c3]))\n",
    "\n",
    "    out = output_layer(h)\n",
    "\n",
    "    model = Model(inputs=[in1, in2, in3], outputs=out)\n",
    "    opt = Adam(lr=0.01)\n",
    "    model.compile(loss='categorical_crossentropy', optimizer=opt)\n",
    "\n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Train the model for a few iterations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/3\n",
      "200297/200297 [==============================] - 4s 20us/step - loss: 2.4007\n",
      "Epoch 2/3\n",
      "200297/200297 [==============================] - 2s 10us/step - loss: 2.0852\n",
      "Epoch 3/3\n",
      "200297/200297 [==============================] - 2s 10us/step - loss: 1.9470\n"
     ]
    }
   ],
   "source": [
    "model = Char3Model(vocab_size, n_factors, n_hidden)\n",
    "history = model.fit(x=[x1, x2, x3], y=y_cat, batch_size=512, epochs=3, verbose=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In order to make sense of the model's output, we need a helper function that converts the character probability array that it returns into an actual character.  This is where the reverse lookup table we created earlier comes in handy!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'e'"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def get_next_char(model, s):\n",
    "    idxs = [np.array([char_indices[c]]) for c in s]\n",
    "    pred = model.predict(idxs)\n",
    "    char_idx = np.argmax(pred)\n",
    "    return chars[char_idx]\n",
    "\n",
    "get_next_char(model, ' th')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "' '"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "get_next_char(model, 'and')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It appears to be spitting out sensible results.  The 3-character approach is very limiting though.  That's not enough context for even a full word most of the time.  For our next step, let's expand the input window to 8 characters.  We can create an input array using some list comprehension magic to output a list of lists, then stacking them together into an array.  Try experimenting with the logic below yourself to get a better sense of what it's doing.  The target array is created in a similar manner as before."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "cs = 8\n",
    "\n",
    "c_in = [[idx[i + j] for i in range(cs)] for j in range(len(idx) - cs)]\n",
    "c_out = [idx[j + cs] for j in range(len(idx) - cs)]\n",
    "\n",
    "X = np.stack(c_in, axis=0)\n",
    "y = np.stack(c_out)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice this time we're making better use of our data by making the sequences overlapping.  For example, the first \"row\" in the data uses characters 0-7 to predict character 8.  The next \"row\" uses characters 1-8 to predict character 9, and so on.  We just increment by one each time.  It does create a lot of duplicate data, but that's not a huge issue with a corpus of this size."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((600885, 8), (600885,))"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X.shape, y.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It helps to look at an example to see how the data is formatted.  Each row is a sequence of 8 characters from the text.  As you go down the rows it's apparent they're offset by one character."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[42, 44, 31, 32, 27, 29, 31,  0],\n",
       "       [44, 31, 32, 27, 29, 31,  0,  0],\n",
       "       [31, 32, 27, 29, 31,  0,  0,  0],\n",
       "       [32, 27, 29, 31,  0,  0,  0, 45],\n",
       "       [27, 29, 31,  0,  0,  0, 45, 47],\n",
       "       [29, 31,  0,  0,  0, 45, 47, 42],\n",
       "       [31,  0,  0,  0, 45, 47, 42, 42],\n",
       "       [ 0,  0,  0, 45, 47, 42, 42, 41]])"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X[:cs, :cs]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([ 0,  0, 45, 47, 42, 42, 41, 45])"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "y[:cs]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Since we have separate inputs for each character, Keras expects separate arrays rather than one big array.  Also need to one-hot encode the target again."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_array = [X[:, i] for i in range(X.shape[1])]\n",
    "y_cat = keras.utils.to_categorical(y)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The 8-character model works exactly the same way as the 3-character model, there are just more of the same steps.  Rather than write them all out in code, I converted it to a loop.  Again, this is almost exactly the way an RNN works under the hood."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "def CharLoopModel(vocab_size, n_chars, n_factors, n_hidden):\n",
    "    embed_layer = Embedding(vocab_size, n_factors)\n",
    "    reshape_layer = Reshape((n_factors,))\n",
    "    input_layer = Dense(n_hidden, activation='relu')\n",
    "    hidden_layer = Dense(n_hidden, activation='tanh')\n",
    "    output_layer = Dense(vocab_size, activation='softmax')\n",
    "    \n",
    "    inputs = []\n",
    "    for i in range(n_chars):\n",
    "        inp = Input(shape=(1,))\n",
    "        inputs.append(inp)\n",
    "        c = input_layer(reshape_layer(embed_layer(inp)))\n",
    "        if i == 0:\n",
    "            h = hidden_layer(c)\n",
    "        else:\n",
    "            h = hidden_layer(add([h, c]))\n",
    "\n",
    "    out = output_layer(h)\n",
    "\n",
    "    model = Model(inputs=inputs, outputs=out)\n",
    "    opt = Adam(lr=0.001)\n",
    "    model.compile(loss='categorical_crossentropy', optimizer=opt)\n",
    "\n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Train the model a bit and generate some predictions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/5\n",
      "600885/600885 [==============================] - 9s 15us/step - loss: 2.1838\n",
      "Epoch 2/5\n",
      "600885/600885 [==============================] - 8s 13us/step - loss: 1.7245\n",
      "Epoch 3/5\n",
      "600885/600885 [==============================] - 8s 13us/step - loss: 1.5888\n",
      "Epoch 4/5\n",
      "600885/600885 [==============================] - 8s 13us/step - loss: 1.5200\n",
      "Epoch 5/5\n",
      "600885/600885 [==============================] - 8s 13us/step - loss: 1.4781\n"
     ]
    }
   ],
   "source": [
    "model = CharLoopModel(vocab_size, cs, n_factors, n_hidden)\n",
    "history = model.fit(x=X_array, y=y_cat, batch_size=512, epochs=5, verbose=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'e'"
      ]
     },
     "execution_count": 22,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "get_next_char(model, 'for thos')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'n'"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "get_next_char(model, 'queens a')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we're ready to replace the loop with a real recurrent layer.  The first thing to notice is that we no longer need to create separate inputs for each step in the sequence - recurrent layers in Keras are designed to accept 3-dimensional arrays where the 2nd dimension is the number of timesteps.  We just need to add an extra dimension to the input shape with the number of characters.\n",
    "\n",
    "The second wrinkle is the use of the \"TimeDistributed\" class on the embedding layer.  Just as with the input, this is another more convenient way of doing what we were already doing by defining and re-using layers.  Wrapping a layer with \"TimeDistributed\" basically says \"apply this to every timestep in the array\".  Like the RNN, it expects (and returns) a 3-dimensional array.  The reshape operation is the same story, we just add another dimension to it.  The RNN layer itself is very straightforward."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.layers import TimeDistributed, SimpleRNN\n",
    "\n",
    "def CharRnn(vocab_size, n_chars, n_factors, n_hidden):\n",
    "    i = Input(shape=(n_chars, 1))\n",
    "    x = TimeDistributed(Embedding(vocab_size, n_factors))(i)\n",
    "    x = Reshape((n_chars, n_factors))(x)\n",
    "    x = SimpleRNN(n_hidden, activation='tanh')(x)\n",
    "    x = Dense(vocab_size, activation='softmax')(x)\n",
    "\n",
    "    model = Model(inputs=i, outputs=x)\n",
    "    opt = Adam(lr=0.001)\n",
    "    model.compile(loss='categorical_crossentropy', optimizer=opt)\n",
    "\n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's look at a summary of the model.  Notice the array shapes have a third dimension to them until we get on the other side of the RNN."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "_________________________________________________________________\n",
      "Layer (type)                 Output Shape              Param #   \n",
      "=================================================================\n",
      "input_12 (InputLayer)        (None, 8, 1)              0         \n",
      "_________________________________________________________________\n",
      "time_distributed_1 (TimeDist (None, 8, 1, 42)          2394      \n",
      "_________________________________________________________________\n",
      "reshape_3 (Reshape)          (None, 8, 42)             0         \n",
      "_________________________________________________________________\n",
      "simple_rnn_1 (SimpleRNN)     (None, 256)               76544     \n",
      "_________________________________________________________________\n",
      "dense_7 (Dense)              (None, 57)                14649     \n",
      "=================================================================\n",
      "Total params: 93,587\n",
      "Trainable params: 93,587\n",
      "Non-trainable params: 0\n",
      "_________________________________________________________________\n"
     ]
    }
   ],
   "source": [
    "model = CharRnn(vocab_size, cs, n_factors, n_hidden)\n",
    "model.summary()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Reshape the input to match the 3-dimensional input format of (rows, timesteps, features).  Since we only have one feature, the last dimension is trivially set to one."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(600885, 8, 1)"
      ]
     },
     "execution_count": 26,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X = X.reshape((X.shape[0], cs, 1))\n",
    "X.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Train the model for a bit.  Notice that the loss looks almost identical to the last model!  All we really did is shuffle things around to take advantage of some built-in classes that Keras provides.  The model structure and performance should look no different than before."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/5\n",
      "600885/600885 [==============================] - 11s 18us/step - loss: 2.2863\n",
      "Epoch 2/5\n",
      "600885/600885 [==============================] - 11s 18us/step - loss: 1.8356\n",
      "Epoch 3/5\n",
      "600885/600885 [==============================] - 10s 17us/step - loss: 1.6601\n",
      "Epoch 4/5\n",
      "600885/600885 [==============================] - 10s 17us/step - loss: 1.5672\n",
      "Epoch 5/5\n",
      "600885/600885 [==============================] - 10s 17us/step - loss: 1.5102\n"
     ]
    }
   ],
   "source": [
    "history = model.fit(x=X, y=y_cat, batch_size=512, epochs=5, verbose=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can train it a bit longer at a lower learning rate to reduce the loss further."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/3\n",
      "600885/600885 [==============================] - 10s 17us/step - loss: 1.4350\n",
      "Epoch 2/3\n",
      "600885/600885 [==============================] - 10s 17us/step - loss: 1.4233\n",
      "Epoch 3/3\n",
      "600885/600885 [==============================] - 10s 17us/step - loss: 1.4175\n"
     ]
    }
   ],
   "source": [
    "K.set_value(model.optimizer.lr, 0.0001)\n",
    "history = model.fit(x=X, y=y_cat, batch_size=512, epochs=3, verbose=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'e'"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def get_next_char(model, s):\n",
    "    idxs = np.array([char_indices[c] for c in s])\n",
    "    idxs = idxs.reshape((1, idxs.shape[0], 1))\n",
    "    pred = model.predict(idxs)\n",
    "    char_idx = np.argmax(pred)\n",
    "    return chars[char_idx]\n",
    "\n",
    "get_next_char(model, 'for thos')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Since the model is getting better, we can now try to generate more than one character of text.  All we need is an initial seed of 8 characters and it can go on as long as we like.  To do this, we'll create a simple helper function that continuously predicts the next character using the last 8 characters that it spit out (starting with the seed value)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'for those who has not to be a conscience of the '"
      ]
     },
     "execution_count": 30,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def get_next_n_chars(model, s, n):\n",
    "    r = s\n",
    "    for i in range(n):\n",
    "        c = get_next_char(model, s)\n",
    "        r += c\n",
    "        s = s[1:] + c\n",
    "    return r\n",
    "\n",
    "get_next_n_chars(model, 'for thos', 40)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "It's definitely getting better.  There are more improvements we can make though!  In the current model, each instance of the data is completely independent.  When a new sequence comes in, the model has no idea what came before that sequence.  That \"hidden state\" mentioned earlier (which is now part of the RNN layer) gets thrown away.  However, there's a way we can set this up that persists that hidden state through to the next part of the sequence.  In other words, it conditions the output not only on the current 8 characters but all the characters that came before it as well.\n",
    "\n",
    "The good news is that this capability is built into Keras's recurrent layers, we just need to set a flag to true!  The bad news is that we need to re-think how the data is structured.  Stateful models require 1) a fixed batch size, which is specified in the model input, and 2) that each batch be a \"slice\" of sequences such that the next batch contains the next part of each sequence.  In other words, we need to split up our data (which is one long continuous stream of text) into n chunks of equal-length streams of text, where n is the batch size.  Then, we need to carve up these n chunks into sequences of length 8 (which is the sequence length the model looks at) with the following character in each sequence being the target (the thing we're predicting).\n",
    "\n",
    "If that sounds confusing and complicated, that's because it is.  It took me a while to make sense of it (and figure out how to express it in code) but hopefully you can follow along.  Below is the first step, which splits the data up into chunks and stacks them vertically into an array.  The result is 64 equal-length continuous sequences of text."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(64, 9388)"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "bs = 64\n",
    "seg_len = len(text) // bs\n",
    "segments = [idx[i*seg_len:(i+1)*seg_len] for i in range(bs)]\n",
    "segments = np.stack(segments)\n",
    "\n",
    "segments.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One other change happening at the same time is we're no longer staggering the input by one character (which duplicates a lot of text because most of it is repeated in each row).  Instead, we're now carving the data into chucks of non-overlapping characters like we did originally. However, we're going to make better use of it this time.  Instead of just predicting character 8 based on characters 0-7, we're going to predict characters 1-8 conditioned on the characters in the sequence that came before them.  Each pass will actually be 8 character predictions, and the loss function will be calculated across all of those outputs (we'll see how to do this in a minute).\n",
    "\n",
    "Below we're creating a list of lists, where each sub-list is an 8-character sequence.  The second list is offset by one (this is our target)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [],
   "source": [
    "c_in = [segments[:,i*cs:(i+1)*cs] for i in range(seg_len // cs)]\n",
    "c_out = [segments[:,(i*cs)+1:((i+1)*cs)+1] for i in range(seg_len // cs)]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we just need to concatenate and reshape these into arrays that we can use with the model.  We end up with ~75,000 chunks of unique 8-character sequences."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = np.concatenate(c_in)\n",
    "X = X.reshape((X.shape[0], X.shape[1], 1))\n",
    "y = np.concatenate(c_out)\n",
    "y_cat = keras.utils.to_categorical(y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((75072, 8, 1), (75072, 8, 57))"
      ]
     },
     "execution_count": 34,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X.shape, y_cat.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Crucially, they are ordered such that the 65th row is a continuation of the 1st row, the 66th row is a continuation of the 2nd row, and so on all the way down."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'preface\\n\\n\\nsupposing that'"
      ]
     },
     "execution_count": 35,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "''.join(indices_char[i] for i in np.concatenate((X[0,:,0], X[64,:,0], X[128,:,0])))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next we can create the stateful RNN model.  It's similar to the last one but there are a few wrinkles.  The input specifies \"batch_shape\" and has three dimensions (this is a hard requirement to use stateful RNNs in Keras, and gets quite annoying during inference time).  We've set \"return_sequences\" to true, which changes the shape that the RNN returns and gives us an output for each step in the sequence.  We've set \"stateful\" to true, the motivation for which was already discussed.  Finally, we've wrapped the last dense layer with \"TimeDistributed\".  This is because the RNN is now returning a higher-dimensional array to account for the output at each timestep.  Everything else works basically the same way."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [],
   "source": [
    "def CharStatefulRnn(vocab_size, n_chars, n_factors, n_hidden, bs):\n",
    "    i = Input(batch_shape=(bs, n_chars, 1))\n",
    "    x = TimeDistributed(Embedding(vocab_size, n_factors))(i)\n",
    "    x = Reshape((n_chars, n_factors))(x)\n",
    "    x = SimpleRNN(n_hidden, activation='tanh', return_sequences=True, stateful=True)(x)\n",
    "    x = TimeDistributed(Dense(vocab_size, activation='softmax'))(x)\n",
    "\n",
    "    model = Model(inputs=i, outputs=x)\n",
    "    opt = Adam(lr=0.001)\n",
    "    model.compile(loss='categorical_crossentropy', optimizer=opt)\n",
    "\n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Looking at the output shapes, we can see the effect of turning on \"return_sequences\".  Note that the number of model parameters has not changed.  The complexity is identical, we've just changed the task and the information available to solve it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "_________________________________________________________________\n",
      "Layer (type)                 Output Shape              Param #   \n",
      "=================================================================\n",
      "input_13 (InputLayer)        (64, 8, 1)                0         \n",
      "_________________________________________________________________\n",
      "time_distributed_2 (TimeDist (64, 8, 1, 42)            2394      \n",
      "_________________________________________________________________\n",
      "reshape_4 (Reshape)          (64, 8, 42)               0         \n",
      "_________________________________________________________________\n",
      "simple_rnn_2 (SimpleRNN)     (64, 8, 256)              76544     \n",
      "_________________________________________________________________\n",
      "time_distributed_3 (TimeDist (64, 8, 57)               14649     \n",
      "=================================================================\n",
      "Total params: 93,587\n",
      "Trainable params: 93,587\n",
      "Non-trainable params: 0\n",
      "_________________________________________________________________\n"
     ]
    }
   ],
   "source": [
    "model = CharStatefulRnn(vocab_size, cs, n_factors, n_hidden, bs)\n",
    "model.summary()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One quirk of using stateful RNNs is that we now have to manually reset the model state, it never goes away until we tell it to.  I just created a simple callback that resets the state at the end of every epoch."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.callbacks import Callback\n",
    "\n",
    "class ResetModelState(Callback):    \n",
    "    def on_epoch_end(self, epoch, logs):\n",
    "        self.model.reset_states()\n",
    "\n",
    "reset_state = ResetModelState()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Train the model for a while as before, with the addition of the callback to reset state between epochs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/paperspace/anaconda3/envs/fastai/lib/python3.6/site-packages/tensorflow/python/ops/gradients_impl.py:98: UserWarning: Converting sparse IndexedSlices to a dense Tensor of unknown shape. This may consume a large amount of memory.\n",
      "  \"Converting sparse IndexedSlices to a dense Tensor of unknown shape. \"\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/8\n",
      "75072/75072 [==============================] - 21s 277us/step - loss: 2.2509\n",
      "Epoch 2/8\n",
      "75072/75072 [==============================] - 20s 261us/step - loss: 1.8441\n",
      "Epoch 3/8\n",
      "75072/75072 [==============================] - 20s 263us/step - loss: 1.6865\n",
      "Epoch 4/8\n",
      "75072/75072 [==============================] - 19s 259us/step - loss: 1.6052\n",
      "Epoch 5/8\n",
      "75072/75072 [==============================] - 20s 261us/step - loss: 1.5540\n",
      "Epoch 6/8\n",
      "75072/75072 [==============================] - 20s 262us/step - loss: 1.5186\n",
      "Epoch 7/8\n",
      "75072/75072 [==============================] - 20s 261us/step - loss: 1.4922\n",
      "Epoch 8/8\n",
      "75072/75072 [==============================] - 20s 263us/step - loss: 1.4714\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<keras.callbacks.History at 0x7ff12da6a240>"
      ]
     },
     "execution_count": 39,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model.fit(x=X, y=y_cat, batch_size=bs, epochs=8, verbose=1, callbacks=[reset_state], shuffle=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/3\n",
      "75072/75072 [==============================] - 19s 259us/step - loss: 1.4280\n",
      "Epoch 2/3\n",
      "75072/75072 [==============================] - 19s 259us/step - loss: 1.4191\n",
      "Epoch 3/3\n",
      "75072/75072 [==============================] - 20s 264us/step - loss: 1.4154\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<keras.callbacks.History at 0x7ff1087b2e10>"
      ]
     },
     "execution_count": 40,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "K.set_value(model.optimizer.lr, 0.0001)\n",
    "model.fit(x=X, y=y_cat, batch_size=bs, epochs=3, verbose=1, callbacks=[reset_state], shuffle=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The \"get next\" functions need to be updated since our approach has changed.  One of the annoying things about stateful models is the batch size is fixed, so even when making a prediction it needs an array of the same size, no matter if we just want to predict one sequence.  I got around this with some numpy hackery."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_next_char(model, bs, s):\n",
    "    idxs = np.array([char_indices[c] for c in s])\n",
    "    idxs = idxs.reshape((1, idxs.shape[0], 1))\n",
    "    idxs = np.repeat(idxs, bs, axis=0)\n",
    "    pred = model.predict(idxs, batch_size=bs)\n",
    "    char_idx = np.argmax(pred[0, 7])\n",
    "    return chars[char_idx]\n",
    "\n",
    "def get_next_n_chars(model, bs, s, n):\n",
    "    r = s\n",
    "    for i in range(n):\n",
    "        c = get_next_char(model, bs, s)\n",
    "        r += c\n",
    "        s = s[1:] + c\n",
    "    return r"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'for those in the same the same the same the same'"
      ]
     },
     "execution_count": 42,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "get_next_n_chars(model, bs, 'for thos', 40)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The output is actually a bit worse than before, but we're still using simple RNNs which aren't that great to begin with.  The real fun comes when we make the jump to a more complex unit like the LSTM.  The details of LSTM's are beyond my scope here but there's a great blog post that everyone links to as the canonical explainer for LTMS, which you can find [here](http://colah.github.io/posts/2015-08-Understanding-LSTMs/).  This is the easiest step yet as the only thing we need to do is replace the class name.  The only other change I made is increasing the number of hidden units.  Everything else stays exactly the same."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.layers import LSTM\n",
    "\n",
    "n_hidden = 512\n",
    "\n",
    "def CharStatefulLSTM(vocab_size, n_chars, n_factors, n_hidden, bs):\n",
    "    i = Input(batch_shape=(bs, n_chars, 1))\n",
    "    x = TimeDistributed(Embedding(vocab_size, n_factors))(i)\n",
    "    x = Reshape((n_chars, n_factors))(x)\n",
    "    x = LSTM(n_hidden, return_sequences=True, stateful=True)(x)\n",
    "    x = TimeDistributed(Dense(vocab_size, activation='softmax'))(x)\n",
    "\n",
    "    model = Model(inputs=i, outputs=x)\n",
    "    opt = Adam(lr=0.001)\n",
    "    model.compile(loss='categorical_crossentropy', optimizer=opt)\n",
    "\n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "LSTMs need to train for a bit longer.  We'll do 20 epochs at each learning rate."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/paperspace/anaconda3/envs/fastai/lib/python3.6/site-packages/tensorflow/python/ops/gradients_impl.py:98: UserWarning: Converting sparse IndexedSlices to a dense Tensor of unknown shape. This may consume a large amount of memory.\n",
      "  \"Converting sparse IndexedSlices to a dense Tensor of unknown shape. \"\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/20\n",
      "75072/75072 [==============================] - 30s 401us/step - loss: 2.1748\n",
      "Epoch 2/20\n",
      "75072/75072 [==============================] - 29s 380us/step - loss: 1.6091\n",
      "Epoch 3/20\n",
      "75072/75072 [==============================] - 29s 381us/step - loss: 1.4487\n",
      "Epoch 4/20\n",
      "75072/75072 [==============================] - 28s 379us/step - loss: 1.3695\n",
      "Epoch 5/20\n",
      "75072/75072 [==============================] - 29s 383us/step - loss: 1.3181\n",
      "Epoch 6/20\n",
      "75072/75072 [==============================] - 29s 385us/step - loss: 1.2797\n",
      "Epoch 7/20\n",
      "75072/75072 [==============================] - 29s 382us/step - loss: 1.2500\n",
      "Epoch 8/20\n",
      "75072/75072 [==============================] - 29s 388us/step - loss: 1.2254\n",
      "Epoch 9/20\n",
      "75072/75072 [==============================] - 29s 380us/step - loss: 1.2052\n",
      "Epoch 10/20\n",
      "75072/75072 [==============================] - 28s 377us/step - loss: 1.1886\n",
      "Epoch 11/20\n",
      "75072/75072 [==============================] - 29s 385us/step - loss: 1.1754\n",
      "Epoch 12/20\n",
      "75072/75072 [==============================] - 28s 379us/step - loss: 1.1649\n",
      "Epoch 13/20\n",
      "75072/75072 [==============================] - 29s 385us/step - loss: 1.1563\n",
      "Epoch 14/20\n",
      "75072/75072 [==============================] - 29s 390us/step - loss: 1.1499\n",
      "Epoch 15/20\n",
      "75072/75072 [==============================] - 29s 383us/step - loss: 1.1447\n",
      "Epoch 16/20\n",
      "75072/75072 [==============================] - 28s 377us/step - loss: 1.1404\n",
      "Epoch 17/20\n",
      "75072/75072 [==============================] - 29s 384us/step - loss: 1.1371\n",
      "Epoch 18/20\n",
      "75072/75072 [==============================] - 29s 383us/step - loss: 1.1334\n",
      "Epoch 19/20\n",
      "75072/75072 [==============================] - 28s 379us/step - loss: 1.1328\n",
      "Epoch 20/20\n",
      "75072/75072 [==============================] - 28s 378us/step - loss: 1.1314\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<keras.callbacks.History at 0x7ff108798d68>"
      ]
     },
     "execution_count": 44,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model = CharStatefulLSTM(vocab_size, cs, n_factors, n_hidden, bs)\n",
    "model.fit(x=X, y=y_cat, batch_size=bs, epochs=20, verbose=1, callbacks=[reset_state], shuffle=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/20\n",
      "75072/75072 [==============================] - 29s 382us/step - loss: 1.1015\n",
      "Epoch 2/20\n",
      "75072/75072 [==============================] - 32s 428us/step - loss: 1.0755\n",
      "Epoch 3/20\n",
      "75072/75072 [==============================] - 33s 442us/step - loss: 1.0633\n",
      "Epoch 4/20\n",
      "75072/75072 [==============================] - 30s 406us/step - loss: 1.0552\n",
      "Epoch 5/20\n",
      "75072/75072 [==============================] - 29s 381us/step - loss: 1.0489\n",
      "Epoch 6/20\n",
      "75072/75072 [==============================] - 28s 372us/step - loss: 1.0434\n",
      "Epoch 7/20\n",
      "75072/75072 [==============================] - 28s 372us/step - loss: 1.0392\n",
      "Epoch 8/20\n",
      "75072/75072 [==============================] - 29s 381us/step - loss: 1.0354\n",
      "Epoch 9/20\n",
      "75072/75072 [==============================] - 28s 376us/step - loss: 1.0323\n",
      "Epoch 10/20\n",
      "75072/75072 [==============================] - 28s 379us/step - loss: 1.0293\n",
      "Epoch 11/20\n",
      "75072/75072 [==============================] - 28s 379us/step - loss: 1.0264\n",
      "Epoch 12/20\n",
      "75072/75072 [==============================] - 28s 373us/step - loss: 1.0246\n",
      "Epoch 13/20\n",
      "75072/75072 [==============================] - 28s 376us/step - loss: 1.0224\n",
      "Epoch 14/20\n",
      "75072/75072 [==============================] - 28s 373us/step - loss: 1.0203\n",
      "Epoch 15/20\n",
      "75072/75072 [==============================] - 29s 382us/step - loss: 1.0183\n",
      "Epoch 16/20\n",
      "75072/75072 [==============================] - 28s 376us/step - loss: 1.0162\n",
      "Epoch 17/20\n",
      "75072/75072 [==============================] - 28s 376us/step - loss: 1.0150\n",
      "Epoch 18/20\n",
      "75072/75072 [==============================] - 28s 376us/step - loss: 1.0134\n",
      "Epoch 19/20\n",
      "75072/75072 [==============================] - 28s 377us/step - loss: 1.0125\n",
      "Epoch 20/20\n",
      "75072/75072 [==============================] - 28s 378us/step - loss: 1.0108\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<keras.callbacks.History at 0x7fef1a149978>"
      ]
     },
     "execution_count": 45,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "K.set_value(model.optimizer.lr, 0.0001)\n",
    "model.fit(x=X, y=y_cat, batch_size=bs, epochs=20, verbose=1, callbacks=[reset_state], shuffle=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And now the moment of truth!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "('for those whoever be no longer for their shows that is the basic of the '\n",
      " 'conseque of the conseque once more proves and the same of the consequent, '\n",
      " 'and at the other that is the basic of the conseque perfeaced itself to the '\n",
      " 'sense and self-conseque contemptations of the conseque once still that the '\n",
      " 'great people take a soul as a profoundination and an artistic as something '\n",
      " 'might be the most problem and self-co')\n"
     ]
    }
   ],
   "source": [
    "pprint(get_next_n_chars(model, bs, 'for thos', 400))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ha, well I wouldn't quite call it sensible but it's not super-terrible either.  It's forming mostly complete words, occasionally using punctuation, etc.  Not bad for being trained one character at a time.  There are many ways that this can be improved of course, but hopefully this has illustrated the key concepts to building a sequence model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
